/*
 *  Copyright 2017 Google Inc. All Rights Reserved.
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.google.blockly.ohos;

import com.google.blockly.ohos.clipboard.BlockClipDataHelper;
import com.google.blockly.ohos.clipboard.SingleMimeTypeClipDataHelper;
import com.google.blockly.ohos.codegen.CodeGenerationRequest;
import com.google.blockly.ohos.codegen.CodeGeneratorManager;
import com.google.blockly.ohos.codegen.LanguageDefinition;
import com.google.blockly.ohos.control.BlocklyController;
import com.google.blockly.ohos.ui.*;
import com.google.blockly.model.Block;
import com.google.blockly.model.BlockExtension;
import com.google.blockly.model.BlockFactory;
import com.google.blockly.model.BlocklySerializerException;
import com.google.blockly.model.CustomCategory;
import com.google.blockly.model.DefaultBlocks;
import com.google.blockly.model.Mutator;
import com.google.blockly.model.Workspace;
import com.google.blockly.ohos.ui.mutator.IfElseMutatorFraction;
import com.google.blockly.utils.BlockLoadingException;
import com.google.blockly.utils.LogUtils;
import com.google.blockly.utils.StringOutputStream;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.fraction.Fraction;
import ohos.aafwk.ability.fraction.FractionManager;
import ohos.agp.components.Component;
import ohos.agp.utils.LayoutAlignment;
import ohos.agp.window.dialog.CommonDialog;
import ohos.agp.window.dialog.ToastDialog;
import ohos.app.Context;
import ohos.global.resource.Resource;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;

/**
 * Class to facilitate Blockly setup on an ability.
 * <p>
 * {@link BlocklyAbilityHelper} looks for
 * the {@link WorkspaceFraction}, the toolbox's {@link BlockListUI}, and the trash's
 * {@link BlockListUI} (such as {@link FlyoutFraction}), and also act as a block drop target to
 * delete blocks.  The methods {@link #onConfigureTrashIcon()}, {@link #onConfigureZoomInButton()},
 * {@link #onConfigureZoomOutButton()}, and {@link #onConfigureCenterViewButton()} will search for
 * these views and set the respective behavior.
 */

public class BlocklyAbilityHelper {
    private static final String TAG = "BlocklyAbilityHelper";

    protected WorkspaceHelper mWorkspaceHelper;
    protected BlockViewFactory mBlockViewFactory;
    protected BlockClipDataHelper mClipDataHelper;
    protected WorkspaceFraction mWorkspaceFraction;
    protected BlockListUI mToolboxBlockList;
    protected BlockListUI mTrashBlockList;
    protected CategorySelectorFraction mCategoryFraction;
    protected Mutator mCurrMutator;
    protected CommonDialog mDialogFraction;
    protected Ability mAbility;

    protected BlocklyController mController;
    protected CodeGeneratorManager mCodeGeneratorManager;

    protected MutatorFraction.DismissListener mMutatorDismissListener = new MutatorFraction
            .DismissListener() {
        @Override
        public void onDismiss(MutatorFraction fraction) {
            if (fraction == mDialogFraction) {
                mCurrMutator = null;
                mDialogFraction = null;
            } else {
                LogUtils.d(TAG, "Received dismiss call for unknown dialog");
            }
        }
    };

    public BlocklyAbilityHelper(Ability ability, FractionManager manager, int scrollOrientation, boolean closeable) {
        mAbility = ability;
        onFindFractions(manager, scrollOrientation, closeable);
        if (mWorkspaceFraction == null) {
            throw new IllegalStateException("mWorkspaceFraction is null");
        }

        mWorkspaceHelper = new WorkspaceHelper(ability);
        mBlockViewFactory = onCreateBlockViewFactory(mWorkspaceHelper);
        mClipDataHelper = onCreateClipDataHelper();
        mCodeGeneratorManager = new CodeGeneratorManager(ability);

        BlocklyController.Builder builder = new BlocklyController.Builder(ability)
                .setClipDataHelper(mClipDataHelper)
                .setWorkspaceHelper(mWorkspaceHelper)
                .setBlockViewFactory(mBlockViewFactory)
                .setWorkspaceFraction(mWorkspaceFraction)
                .setTrashUi(mTrashBlockList)
                .setToolboxUi(mToolboxBlockList, mCategoryFraction);
        mController = builder.build();

        onCreateVariableCallback();
        onCreateMutatorListener();

        onConfigureTrashIcon();
        onConfigureZoomInButton();
        onConfigureZoomOutButton();
        onConfigureCenterViewButton();
    }

    public void onStart() {
        // Do nothing.
    }

    public void onResume() {
        mCodeGeneratorManager.onResume();
    }

    public BlocklyController getController() {
        return mController;
    }

    public void saveWorkspaceToAppDir(String filename)
            throws FileNotFoundException, BlocklySerializerException {
        Workspace workspace = mWorkspaceFraction.getWorkspace();
        FileOutputStream out = new FileOutputStream(mAbility.getExternalCacheDir() + "/" + filename);
        workspace.serializeToXml(out);
    }

    /**
     * Save the workspace to the given file in the application's private data directory, and show a
     * status toast. If the save fails, the error is logged.
     *
     * @return True if the save was successful. Otherwise false.
     */
    public boolean saveWorkspaceToAppDirSafely(String filename) {
        try {
            saveWorkspaceToAppDir(filename);
            new ToastDialog(mAbility)
                    .setText(mAbility.getString(ResourceTable.String_toast_workspace_saved))
                    .setAlignment(LayoutAlignment.CENTER)
                    .setDuration(1000)
                    .show();
            return true;
        } catch (FileNotFoundException | BlocklySerializerException e) {
            new ToastDialog(mAbility)
                    .setText(mAbility.getString(ResourceTable.String_toast_workspace_not_saved))
                    .setAlignment(LayoutAlignment.CENTER)
                    .setDuration(1000)
                    .show();
            LogUtils.e(TAG, "Failed to save workspace to " + filename);
            return false;
        }
    }

    /**
     * Loads the workspace from the given file in the application's private data directory.
     *
     * @param filename The path to the file, in the application's local storage.
     * @throws IOException           If there is a underlying problem with the input.
     * @throws BlockLoadingException If there is a error with the workspace XML format or blocks.
     */
    public void loadWorkspaceFromAppDir(String filename) throws IOException, BlockLoadingException {
        FileInputStream in = new FileInputStream(mAbility.getExternalCacheDir() + "/" + filename);
        mController.loadWorkspaceContents(in);
    }

    /**
     * Loads the workspace from the given file in the application's private data directory. If it
     * fails to load, a toast will be shown and the error will be logged.
     *
     * @param filename The path to the file, in the application's local storage.
     * @return True if loading the workspace succeeds. Otherwise, false and the error will be
     * logged.
     */
    public boolean loadWorkspaceFromAppDirSafely(String filename) {
        try {
            loadWorkspaceFromAppDir(filename);
            return true;
        } catch (FileNotFoundException e) {
            new ToastDialog(mAbility)
                    .setText(mAbility.getString(ResourceTable.String_toast_workspace_file_not_found))
                    .setAlignment(LayoutAlignment.CENTER)
                    .setDuration(1000)
                    .show();
        } catch (IOException | BlockLoadingException e) {
            new ToastDialog(mAbility)
                    .setText(mAbility.getString(ResourceTable.String_toast_workspace_load_failed))
                    .setAlignment(LayoutAlignment.CENTER)
                    .setDuration(1000)
                    .show();
            LogUtils.e(TAG, "Failed to load workspace");
        }
        return false;
    }

    /**
     * @return True if the action was handled to close a previously open (and closable) toolbox,
     * trash UI, or dialog. Otherwise false.
     */
    public boolean onBackToCloseFlyouts() {
        return closeDialogFraction() || mController.closeFlyouts();
    }

    /**
     * Requests code generation using the blocks in the {@link Workspace}/{@link WorkspaceFraction}.
     *
     * @param codeGeneratorLanguage     The language definition for the generators.
     * @param blockDefinitionsJsonPaths The asset path to the JSON block definitions.
     * @param generatorsJsPaths         The asset paths to the JavaScript generators, and optionally the
     *                                  JavaScript block extension/mutator sources.
     * @param codeGenerationCallback    The {@link CodeGenerationRequest.CodeGeneratorCallback} to use
     *                                  upon completion.
     */
    public void requestCodeGeneration(
            LanguageDefinition codeGeneratorLanguage,
            List<String> blockDefinitionsJsonPaths,
            List<String> generatorsJsPaths,
            CodeGenerationRequest.CodeGeneratorCallback codeGenerationCallback) {

        final StringOutputStream serialized = new StringOutputStream();
        try {
            mController.getWorkspace().serializeToXml(serialized);
        } catch (BlocklySerializerException e) {
            // Not using a string resource because no non-developer should see this.
            String msg = "Failed to serialize workspace during code generation.";
            LogUtils.e(TAG, msg);
            new ToastDialog(mAbility)
                    .setText(msg)
                    .setAlignment(LayoutAlignment.CENTER)
                    .setDuration(1000)
                    .show();
        }

        mCodeGeneratorManager.requestCodeGeneration(
                new CodeGenerationRequest(
                        serialized.toString(),
                        codeGenerationCallback,
                        codeGeneratorLanguage,
                        blockDefinitionsJsonPaths,
                        generatorsJsPaths));
        try {
            serialized.close();
        } catch (IOException e) {
            // Ignore error on close().
        }
    }


    public void onPause() {
        mCodeGeneratorManager.onPause();
    }


    public void onStop() {
        // Do nothing.
    }

    public void onRestart() {
        // Do nothing.
    }

    public void onDestroy() {
        // Do nothing.
    }

    /**
     * Default implementation for {@link AbstractBlocklyAbility#configureMutators()}. This adds
     * all mutators in {@link DefaultBlocks} and their UIs to the {@link BlockFactory} and
     * {@link BlockViewFactory}.
     * <p/>
     * Mutators with the same key will replace existing mutators, so it is safe
     * to call super and then update specific mutators.
     */
    public void configureMutators() {
        BlockFactory blockFactory = mController.getBlockFactory();
        Map<String, Mutator.Factory> defaultMutators = DefaultBlocks.getMutators();
        for (String key : defaultMutators.keySet()) {
            blockFactory.registerMutator(key, defaultMutators.get(key));
        }

        Map<String, MutatorFraction.Factory> defaultMutatorUis = DefaultBlocks.getMutatorUis();
        for (String key : defaultMutatorUis.keySet()) {
            mBlockViewFactory.registerMutatorUi(key, defaultMutatorUis.get(key));
        }
    }

    /**
     * Default implementation for {@link AbstractBlocklyAbility#configureBlockExtensions()}. This
     * adds all extensions in {@link DefaultBlocks} to the {@link BlockFactory}.
     * <p/>
     * Extensions with the same key will replace existing extensions, so it is safe
     * to call super and then update specific extensions.
     */
    public void configureExtensions() {
        BlockFactory blockFactory = mController.getBlockFactory();
        Map<String, BlockExtension> extensions = DefaultBlocks.getExtensions();
        for (String key : extensions.keySet()) {
            blockFactory.registerExtension(key, extensions.get(key));
        }
    }

    /**
     * This method provides a hook to register custom {@link CustomCategory}s that support
     * toolboxes in this ability. The default implementation registers
     * {@link DefaultBlocks#getToolboxCustomCategories the categories in DefaultBlocks}.
     * <p/>
     * Most subclasses will want to include these default custom categories, calling
     * {@code super.configureCategoryFactories()} if overridden. Category factories with the same
     * {@code custom} key will replace existing {@link CustomCategory}s, so it is safe to call
     * super and then update specific categories.
     */
    public void configureCategoryFactories() {
        Map<String, CustomCategory> factoryMap =
                DefaultBlocks.getToolboxCustomCategories(mController);
        for (String key : factoryMap.keySet()) {
            mController.registerCategoryFactory(key, factoryMap.get(key));
        }
    }

    /**
     * Shows a dialog UI to the user. Only one dialog will be shown at a time and if there is
     * already an existing one it will be replaced. This can be used for showing application or
     * block specific UIs, such as a mutator UI. When the user finishes using the fraction
     * {@link #closeDialogFraction()} should be called.
     *
     * @param fraction The fraction to show.
     */
    public void showDialogFraction(CommonDialog fraction) {
        if (fraction == mDialogFraction) {
            return;
        }

//        FractionTransaction ft = mAbility.getSupportFractionManager().beginTransaction();
//        if (mDialogFraction != null) {
//            ft.remove(mDialogFraction);
//        }
//        fraction.show(ft, "blockly_dialog");
        mDialogFraction = fraction;
    }

    /**
     * @return True if there was a dialog and it was closed, false otherwise.
     */
    public boolean closeDialogFraction() {
        if (mDialogFraction != null) {
//            FractionTransaction ft = mAbility.getSupportFractionManager().beginTransaction();
//            ft.remove(mDialogFraction);
//            ft.commit();
            mDialogFraction = null;
            return true;
        }
        return false;
    }

    protected void onFindFractions(FractionManager fractionManager, int scrollOrientation, boolean closeable) {
        mWorkspaceFraction = new WorkspaceFraction();
        fractionManager.startFractionScheduler().add(ResourceTable.Id_blockly_workspace, mWorkspaceFraction).submit();
        mToolboxBlockList = new FlyoutFraction(scrollOrientation, closeable);
        fractionManager.startFractionScheduler().add(ResourceTable.Id_blockly_toolbox_ui, (Fraction) mToolboxBlockList).submit();
        mCategoryFraction = new CategorySelectorFraction(scrollOrientation);
        fractionManager.startFractionScheduler().add(ResourceTable.Id_blockly_categories, mCategoryFraction).submit();
        mTrashBlockList = new FlyoutFraction(scrollOrientation, closeable);
        fractionManager.startFractionScheduler().add(ResourceTable.Id_blockly_trash_ui, (Fraction) mTrashBlockList).submit();
        if (mTrashBlockList != null) {
            // TODO(#14): Make trash list a drop location.
        }
    }

    /**
     * Constructs the {@link BlockViewFactory} used by all fractions in this ability. The Blockly
     * core library does not include a factory implementation, and the app developer will need to
     * include blockly vertical or another block rendering implementation.
     * <p>
     * The default implementation attempts to instantiates a VerticalBlockViewFactory, which is
     * included in the blocklylib-vertical library.  An error will be thrown unless
     * blocklylib-vertical is included or this method is overridden to provide a custom
     * BlockViewFactory.
     *
     * @param helper The Workspace helper for this ability.
     * @return The {@link BlockViewFactory} used by all fractions in this ability.
     */
    public BlockViewFactory onCreateBlockViewFactory(WorkspaceHelper helper) {
        try {
            @SuppressWarnings("unchecked")
            Class<? extends BlockViewFactory> clazz =
                    (Class<? extends BlockViewFactory>) Class.forName(
                            "com.google.blockly.ohos.ui.vertical.VerticalBlockViewFactory");
            return clazz.getDeclaredConstructor(Context.class, WorkspaceHelper.class)
                    .newInstance(mAbility.getContext(), helper);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(
                    "Default BlockViewFactory not found. Did you include blocklylib-vertical?", e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Unable to instantiate VerticalBlockViewFactory", e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to instantiate VerticalBlockViewFactory", e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to instantiate VerticalBlockViewFactory", e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Unable to instantiate VerticalBlockViewFactory", e);
        }
    }

    /**
     * Constructs the {@link BlockClipDataHelper} for use by all Blockly components of
     * {@link #mAbility}. The instance will be passed to the controller and available via
     * {@link BlocklyController#getClipDataHelper()}.
     * <p/>
     * By default, it constructs a {@link SingleMimeTypeClipDataHelper} with a MIME type derived
     * from the application's package name. This assumes all Blockly workspaces in an app work with
     * the same shared set of blocks, and blocks can be dragged/copied/pasted between them, even if
     * they are in different Activities. It also ensures blocks from other applications will be
     * rejected.
     * <p/>
     * If your app uses different block sets for different workspaces, or you intend to interoperate
     * with other applications, you will need to override this method with your own implementation.
     *
     * @return A new {@link BlockClipDataHelper}.
     */
    protected BlockClipDataHelper onCreateClipDataHelper() {
        return SingleMimeTypeClipDataHelper.getDefault(mAbility);
    }


    protected void onConfigureTrashIcon() {
        TrashCanView trashIcon = (TrashCanView) mAbility.findComponentById(ResourceTable.Id_blockly_trash_icon);
        if (mController != null && trashIcon != null) {
            mController.setTrashIcon(trashIcon);
        }
    }

    protected void onConfigureZoomInButton() {
        Component zoomInButton = mAbility.findComponentById(ResourceTable.Id_blockly_zoom_in_button);
        if (zoomInButton != null) {
            zoomInButton.setClickedListener(component -> mController.zoomIn());
            ZoomBehavior zoomBehavior = mWorkspaceHelper.getZoomBehavior();
            zoomInButton.setVisibility(zoomBehavior.isButtonEnabled() ? Component.VISIBLE : Component.INVISIBLE);
        }
    }

    protected void onConfigureZoomOutButton() {
        Component zoomOutButton = mAbility.findComponentById(ResourceTable.Id_blockly_zoom_out_button);
        if (zoomOutButton != null) {
            zoomOutButton.setClickedListener(component -> mController.zoomOut());
            ZoomBehavior zoomBehavior = mWorkspaceHelper.getZoomBehavior();
            zoomOutButton.setVisibility(zoomBehavior.isButtonEnabled() ? Component.VISIBLE : Component.INVISIBLE);
        }
    }

    protected void onConfigureCenterViewButton() {
        Component recenterButton = mAbility.findComponentById(ResourceTable.Id_blockly_center_view_button);
        if (recenterButton != null) {
            recenterButton.setClickedListener(component -> mController.recenterWorkspace());
            ZoomBehavior zoomBehavior = mWorkspaceHelper.getZoomBehavior();
            recenterButton.setVisibility(zoomBehavior.isFixed() ? Component.INVISIBLE : Component.VISIBLE);
        }
    }

    /**
     * Constructs a {@link BlocklyController.VariableCallback} for handling user requests to change
     * the list of variables (create, rename, delete). This can be used to provide UI for confirming
     * a deletion or renaming a variable.
     * <p/>
     * By default, this method constructs a {@link DefaultVariableCallback}. Apps can override this
     * to provide an alternative implementation, or optionally override the method to do nothing (no
     * confirmation UI).
     * <p/>
     * This method is responsible for calling {@link BlocklyController#setVariableCallback}.
     *
     * @return A {@link BlocklyController.VariableCallback} for
     * handling variable updates from the controller.
     */
    protected void onCreateVariableCallback() {
        BlocklyController.VariableCallback variableCb =
                new DefaultVariableCallback(mAbility, mController);
        mController.setVariableCallback(variableCb);
    }

    /**
     * Constructs a {@link BlockViewFactory.MutatorToggleListener} for handling user requests to
     * show/hide a mutator for a block.
     * <p/>
     * By default, this method will remove the open dialog Fraction if there is one and show the
     * mutator's Fraction if it wasn't just removed.
     * <p/>
     * This method is responsible for calling {@link BlockViewFactory#setMutatorToggleListener}.
     */
    protected void onCreateMutatorListener() {
        BlockViewFactory.MutatorToggleListener mutatorListener = new BlockViewFactory
                .MutatorToggleListener() {
            @Override
            public void onMutatorToggled(Block block) {
                handleMutatorToggled(block);
            }
        };
        mBlockViewFactory.setMutatorToggleListener(mutatorListener);
    }

    /**
     * Resets the {@link BlockFactory} with the provided block definitions and extensions.
     *
     * @param blockDefinitionsJsonPaths The list of definition asset paths.
     * @throws IllegalStateException On any issues with the input.
     */
    public void resetBlockFactory(List<String> blockDefinitionsJsonPaths) {
        BlockFactory factory = mController.getBlockFactory();
        factory.clear();
        String assetPath = null;
        try {
            if (blockDefinitionsJsonPaths != null) {
                for (String path : blockDefinitionsJsonPaths) {
                    assetPath = "entry/resources/rawfile/" + path;
                    Resource resource = mAbility.getResourceManager().getRawFileEntry(assetPath).openRawFile();
                    factory.addJsonDefinitions(resource);
                }
            }
        } catch (IOException | BlockLoadingException e) {
            throw new IllegalStateException(
                    "Failed to load block definition asset file: " + assetPath, e);
        }
    }

    /**
     * Reloads the toolbox from assets.
     *
     * @param toolboxContentsXmlPath The asset path to the toolbox XML
     * @throws IllegalStateException If error occurs during loading.
     */
    public void reloadToolbox(String toolboxContentsXmlPath) {
        BlocklyController controller = getController();
        try {
            toolboxContentsXmlPath = "entry/resources/rawfile/" + toolboxContentsXmlPath;
            Resource resource = mAbility.getResourceManager().getRawFileEntry(toolboxContentsXmlPath).openRawFile();
            controller.loadToolboxContents(resource);
        } catch (IOException | BlockLoadingException e) {
            LogUtils.e(TAG, "Failed to load toolbox XML");
        }
    }


    /**
     * Decide when and how to show UI for a mutator. This should be called when the user has tried
     * to toggle a mutator. The method will figure out if the mutator's dialog should be shown,
     * hidden, or ignored in response.
     *
     * @param block The block the user is toggling a mutator for.
     */
    private void handleMutatorToggled(Block block) {
        Mutator mutator = block.getMutator();
//        if (mutator == mCurrMutator) {
//            if (mDialogFraction != null) {
//                FractionTransaction ft = mAbility.getSupportFractionManager().beginTransaction();
//                ft.remove(mDialogFraction);
//                ft.commit();
//            } else {
//                Log.w(TAG, "Had a mutator, but no dialog Fraction to remove. Cleaning up state.");
//            }
//                mCurrMutator = null;
//            }
        if (!mBlockViewFactory.hasUiForMutator(mutator.getMutatorId())) {
            LogUtils.e(TAG, "Mutator without UI toggled.");
            return;
        }
        IfElseMutatorFraction fraction = (IfElseMutatorFraction) mBlockViewFactory.getMutatorFraction(mutator);
        if (fraction == null) {
            throw new IllegalArgumentException("Mutator with UI has no Dialog Fraction to show.");
        }
        fraction.show();
//
//        FractionTransaction ft = mAbility.getSupportFractionManager().beginTransaction();
//        if (mDialogFraction != null) {
//            ft.remove(mDialogFraction);
//        }
//        fraction.show(ft, "MUTATOR_DIALOG");
//        fraction.setDismissListener(mMutatorDismissListener);
//            mCurrMutator = mutator;
//            mDialogFraction = fraction;
//        }
    }
}
